// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fbl/alloc_checker.h>
#include <fbl/auto_call.h>
#include <pretty/hexdump.h>

#include <utility>

#include "debug-logging.h"
#include "usb-audio-descriptors.h"

namespace audio {
namespace usb {

DescriptorListMemory::~DescriptorListMemory() {
  // According to docs in the header, data allocated using
  // usb_get_descriptor_list should be free'ed using a standard C "free"
  if (data_ != nullptr) {
    free(data_);
  }
}

fbl::RefPtr<DescriptorListMemory> DescriptorListMemory::Create(usb_protocol_t* proto) {
  ZX_DEBUG_ASSERT(proto != nullptr);

  fbl::AllocChecker ac;
  auto ret = fbl::AdoptRef(new (&ac) DescriptorListMemory());
  if (!ac.check()) {
    GLOBAL_LOG(ERROR, "Failed to allocate descriptor list memory!");
    return nullptr;
  }

  size_t desc_length = usb_get_descriptors_length(proto);
  ret->data_ = malloc(desc_length);
  if (!ret->data_) {
    return nullptr;
  }
  usb_get_descriptors(proto, ret->data_, desc_length, &ret->size_);

  if (zxlog_level_enabled(SPEW)) {
    GLOBAL_LOG(SPEW, "Descriptor List is %zu bytes long\n", ret->size_);
    hexdump8_ex(ret->data_, ret->size_, 0u);
  }

  return ret;
}

DescriptorListMemory::Iterator::Iterator(fbl::RefPtr<DescriptorListMemory> mem)
    : mem_(std::move(mem)) {
  ZX_DEBUG_ASSERT(mem_ != nullptr);
  // Make sure our offset is valid, or go ahead and invalidate it right from
  // the start.
  ValidateOffset();
}

bool DescriptorListMemory::Iterator::Next() {
  auto h = hdr();

  // Have we already run out of headers?
  if (h == nullptr) {
    return false;
  }

  // Advance to the next header, then validate our offset.  Note that there is
  // no overflow check here.  If we overflow a 64 bit size_t, the implication
  // is that we are holding a USB descriptor list in RAM whose size is within
  // 256 bytes of our entire 64 bit address space.  This really should be
  // impossible, so we don't bother to check.
  offset_ += h->bLength;
  return ValidateOffset();
}

bool DescriptorListMemory::Iterator::ValidateOffset() {
  ZX_DEBUG_ASSERT(offset_ <= mem_->size());
  size_t space = mem_->size() - offset_;

  if (!space) {
    return false;
  }

  // If anything goes wrong from here on out, make sure to invalidate our offset.
  auto cleanup = fbl::MakeAutoCall([this] { offset_ = mem_->size(); });

  if (space < sizeof(usb_descriptor_header_t)) {
    GLOBAL_LOG(WARN,
               "Insufficient space at offset %zu to contain even the most basic USB descriptor "
               "(space needed %zu, space left %zu)\n",
               offset_, sizeof(usb_descriptor_header_t), space);
    return false;
  }

  auto tmp = reinterpret_cast<uintptr_t>(mem_->data());
  auto h = reinterpret_cast<const usb_descriptor_header_t*>(tmp + offset_);
  if (h->bLength > space) {
    GLOBAL_LOG(WARN,
               "Malformed USB descriptor header (type %u) at offset %zu.  "
               "Header indicates that it is %u bytes long, but there %zu bytes remaining\n",
               h->bDescriptorType, offset_, h->bLength, space);
    return false;
  }

  GLOBAL_LOG(SPEW, "Found Descriptor [type 0x%02x, len 0x%02x] at offset 0x%zx/0x%zx\n",
             h->bDescriptorType, h->bLength, offset_, mem_->size());

  cleanup.cancel();
  return true;
}

}  // namespace usb
}  // namespace audio
